package com.ilxqx.framework.security.validation;

import com.google.common.collect.Maps;
import com.ilxqx.framework.security.config.GeeTestConfigProperties;
import com.ilxqx.framework.security.exception.ExpiredValidationException;
import com.ilxqx.framework.security.util.KeyUtils;
import com.ilxqx.framework.util.HttpRequestUtils;
import com.ilxqx.framework.util.RedisUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.ServletRequestUtils;

import javax.servlet.http.HttpServletRequest;
import java.time.Duration;
import java.util.Map;

/**
 * 极验验证实现
 * @author venus
 */
@Service
@Slf4j
public class GeeTestValidation implements Validation {

    /**
     * 交换码长度
     */
    private static final int CHALLENGE_CODE_LENGTH = 32;
    private static final String GEE_TEST_FLAG = "geetest";
    private static final String NAMESPACE = "validation:geetest:";
    private static final String UUID_KEY_NAME = "uuid:";
    private final GeeTestConfigProperties geeTestConfigProperties;

    @Autowired
    public GeeTestValidation(GeeTestConfigProperties geeTestConfigProperties) {
        this.geeTestConfigProperties = geeTestConfigProperties;
    }

    /**
     * 预处理失败
     * @return map
     */
    private Map<String, Object> failPreprocess (String uuid) {

        String strOne = DigestUtils.md5Hex(RandomStringUtils.randomNumeric(2));
        String strTwo = DigestUtils.md5Hex(RandomStringUtils.randomNumeric(2));
        String challenge = strOne + strTwo.substring(0, 2);

        Map<String, Object> map = Maps.newHashMapWithExpectedSize(4);
        map.put("uuid", uuid);
        map.put("success", false);
        map.put("gt", this.geeTestConfigProperties.getCaptchaId());
        map.put("challenge", challenge);

        log.info("宕机验证[{}]", map);
        return map;
    }

    /**
     * 预处理成功
     * @param challenge challenge
     * @return map
     */
    private Map<String, Object> sucPreprocess (String challenge, String uuid) {
        Map<String, Object> map = Maps.newHashMapWithExpectedSize(4);
        map.put("uuid", uuid);
        map.put("success", true);
        map.put("gt", this.geeTestConfigProperties.getCaptchaId());
        map.put("challenge", challenge);
        log.info("处理成功[{}]", map);
        return map;
    }

    private String getUuidKeyName(String uuid) {
        return NAMESPACE + UUID_KEY_NAME + uuid;
    }

    /**
     * 验证前进行准备
     *
     * @param request 请求对象
     * @return 验证需要的参数
     */
    @Override
    public Map<String, Object> challenge(HttpServletRequest request) {
        String uuid = KeyUtils.generateUUID();
        String clientType = ServletRequestUtils.getStringParameter(request, "clientType", "app");
        String ipAddress = request.getRemoteAddr();

        // 拼接URL
        String url = String.format(this.geeTestConfigProperties.getUrl() + this.geeTestConfigProperties.getRegisterUri() + "?gt=%s", this.geeTestConfigProperties.getCaptchaId());
        // 拼接参数
        url += "&user_id=" + uuid;
        url += "&client_type=" + clientType;
        if (ipAddress != null) {
            url += "&ip_address=" + ipAddress;
        }
        log.info("拼接请求获取challenge的地址[{}]", url);
        String res = HttpRequestUtils.get(url);
        log.info("向极验获取challenge返回结果[{}]", res);

        boolean success = res.length() == CHALLENGE_CODE_LENGTH;
        // 将uuid设置到redis中
        RedisUtils.set(this.getUuidKeyName(uuid), success, Duration.ofMinutes(5));

        if (res.length() != CHALLENGE_CODE_LENGTH) {
            log.info("获取到的challenge不正确[{}]", res);
            return this.failPreprocess(uuid);
        }

        return this.sucPreprocess(DigestUtils.md5Hex(res + this.geeTestConfigProperties.getPrivateKey()), uuid);
    }

    private boolean checkResultByPrivate (String challenge, String validate) {
        return StringUtils.equals(validate, DigestUtils.md5Hex(this.geeTestConfigProperties.getPrivateKey() + GEE_TEST_FLAG + challenge));
    }

    /**
     * 二次验证
     *
     * @param request 请求对象
     * @return 验证是否通过
     */
    @Override
    public boolean validate(HttpServletRequest request) {
        String challenge = request.getParameter("challenge");
        String validate = request.getParameter("validate");
        String seccode = request.getParameter("seccode");
        String uuid = request.getParameter("uuid");
        Boolean success = RedisUtils.get(this.getUuidKeyName(uuid));
        if (success == null) {
            // 验证过期
            throw new ExpiredValidationException("验证已经过期，请重新验证");
        }
        String clientType = ServletRequestUtils.getStringParameter(request, "clientType", "app");
        String ipAddress = request.getRemoteAddr();

        if (StringUtils.isAnyBlank(challenge, validate, seccode)) {
            return false;
        }

        if (!success) {
            return true;
        }

        if (!this.checkResultByPrivate(challenge, validate)) {
            log.warn("私钥验证失败");
            return false;
        }

        String url = this.geeTestConfigProperties.getUrl() + this.geeTestConfigProperties.getValidateUri();
        String params = String.format("challenge=%s&validate=%s&seccode=%s",
                challenge, validate, seccode);

        if (uuid != null){
            params += "&user_id=" + uuid;
        }
        params += "&client_type=" + clientType;
        if (ipAddress != null){
            params += "&ip_address=" + ipAddress;
        }

        log.info("二次验证[{}]请求参数[{}]", url, params);
        String res = HttpRequestUtils.post(url, MediaType.APPLICATION_FORM_URLENCODED_VALUE, params);

        log.info("二次验证请求成功[{}]", res);
        return StringUtils.equals(res, DigestUtils.md5Hex(seccode));
    }

}
